using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace SocketsServer {
    public partial class FrmServer : Form {
        Socket socketWatch = null;
        Thread threadWatch = null;

        //保存了服务器端所有负责和客户端通信的套接字
        Dictionary<string, Socket> dictSocket = new Dictionary<string, Socket>();
        //保存服务器端所有负责调用通信套接字.Receive方法的线程
        Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>();
        public static int CFlag = 0;//客户端关闭的标志

        public FrmServer() {
            InitializeComponent();
        }

        //启动监听
        private void btnListen_Click(object sender, EventArgs e) {
            //创建用于通信的套接字,监听客户端发来的消息，包含三个参数（IP4寻址协议，流式连接，Tcp协议）
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //1.绑定IP和Port 
            //这里的IP是long类型的,这里使用Parse()可以将string类型数据转成IPAddress所需要的数据类型
            IPAddress ipaddress = IPAddress.Parse(txtAddr.Text.Trim());
            IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse(txtPort.Text.Trim()));
            try { 
                //2.使用Bind()进行绑定
                //Bind(),绑定一个本地的IP和端口号，参数是一个绑定了IP和端口号的IPEndPoint对象
                socketWatch.Bind(endpoint);
                //3.开启监听
                //Listen(int backlog); backlog:监听数量
                socketWatch.Listen(10);

                /*
                注意：
                Accept会阻碍主线程的运行，一直在等待客户端的请求，
                客户端如果不介入，它就会一直在这里等待，主线程卡死
                所以开启一个新的线程接收客户端的请求
                */
                //开启线程Accept进行通信的客户端socket
                threadWatch = new Thread(WatchingConn);//运行线程在后台执行,将窗体线程设置为与后台同步，随着主线程结束而结束
                threadWatch.IsBackground = true;
                threadWatch.Start();

                this.Invoke(new Action(() => {
                    rchReceive.AppendText("开始对客户端进行监听！" + "\r\n");
                }));
                btnListen.Enabled = false;//禁止操作接收按钮
            } catch {
                MessageBox.Show("服务器连接失败...", "提示", MessageBoxButtons.OK);
            }
        }

        //建立与客户端的连接
        private void WatchingConn(object obj) {
            while (true) {
                //4.阻塞到有client连接
                //Accept(),接收连接并返回一个新的Socket,Accept会中断程序，直到有客户端连接
                Socket socConn = socketWatch.Accept();
                CFlag = 0;//连接成功，将客户端关闭的标志设置为0

                this.Invoke(new Action(() => {
                    cboClientIP.Items.Add(socConn.RemoteEndPoint.ToString());
                }));

                //将与客户端通信的套接字对象socConn添加到键值对集合中，并以客户端IP端口作为键
                dictSocket.Add(socConn.RemoteEndPoint.ToString(), socConn);

                //TreadStart委托仅仅指向无参数且无返回值的方法。
                //如果新线程上运行带参数的方法，那么需要用到ParameterizedThreadStart委托
                ParameterizedThreadStart pts = new ParameterizedThreadStart(RecMsg);

                Thread trd = new Thread(pts);
                trd.IsBackground = true;
                trd.Start(socConn);

                dictThread.Add(socConn.RemoteEndPoint.ToString(), trd);
                this.Invoke(new Action(() => {
                    rchReceive.AppendText("客户端连接成功！" + "\r\n");
                }));
            }
        }

        //接收客户端数据
        private void RecMsg(object socketClientPara) {
            Socket socketRec = socketClientPara as Socket;// 创建用于通信的套接字

            while (true) {
                byte[] arrRecMsg = new byte[1024];

                int length = -1;
                try {
                    //5.接收数据
                    length = socketRec.Receive(arrRecMsg);

                    if (arrRecMsg.Length > 0) { 
                        //如果接收到客户端停止的标志
                        if(Encoding.ASCII.GetString(arrRecMsg, 0, length) == "*close*") {
                            //rchReceive.Text += DateTime.Now.ToString("yy-MM-dd hh:mm:ss  ") + "客户端已退出" + "\n";
                            this.Invoke(new Action(() => {
                                rchReceive.AppendText(GetTime() + "  " + "客户端已退出" + "\r\n");
                                CFlag = 1;//将客户端关闭标志设置为1
                            }));                         
                            break;//退出循环
                        }
                    }
                } catch (SocketException ex) {
                    //MessageBox.Show("异常：" + ex.Message);
                    //从通信套接字集合中删除被中断连接的通信套接字对象
                    dictSocket.Remove(socketRec.RemoteEndPoint.ToString());
                    //从通信线程结合中删除被终端连接的通信线程对象
                    dictThread.Remove(socketRec.RemoteEndPoint.ToString());
                    //从列表中移除被中断的连接 ip:Port
                    cboClientIP.Items.Remove(socketRec.RemoteEndPoint.ToString());
                    break;
                } catch (Exception ex) {
                    MessageBox.Show("异常：" + ex.Message);
                    break;
                }

                string strPort = socketRec.RemoteEndPoint.ToString();
                string str = Encoding.UTF8.GetString(arrRecMsg, 0, length);
                this.Invoke(new Action(() => {
                    //rchReceive.AppendText(cboClientIP.Text + ":\r\n" + GetTime() + "\r\n" + str + "\r\n");
                    rchReceive.AppendText(GetTime() +  "  " + strPort + "：" + str + "\r\n");

                }));

                Thread.Sleep(10);
            }
        }

        //获取当前时间
        private DateTime GetTime() {
            DateTime getTime = new DateTime();
            getTime = DateTime.Now;
            return getTime;
        }

        //向客户端发送数据
        private void SendMsg(string sendMsg) {
            if (string.IsNullOrEmpty(cboClientIP.Text)) {
                MessageBox.Show("请选择通信IP！");
            } else {
                byte[] strSendMsg = Encoding.UTF8.GetBytes(sendMsg);
                string strClientKey = cboClientIP.Text;//通过Key匹配对应ip地址的客户端
                //6.发送数据
                dictSocket[strClientKey].Send(strSendMsg);
                this.Invoke(new Action(() => {
                    //rchReceive.AppendText("服务器:" + "\r\n" + GetTime() + "\r\n" + sendMsg + "\r\n");
                    rchReceive.AppendText(GetTime() + "  " + "Server：" + sendMsg + "\r\n");
                }));
                rchSend.Text = null;
            }
        }
        private void btnSend_Click(object sender, EventArgs e) {
            if(CFlag == 0) { 
                SendMsg(rchSend.Text.Trim());
            }
        }

        //关闭服务器端
        private void btnCloseServer_Click(object sender, EventArgs e) {
            if(CFlag == 1) {
                //threadWatch.Abort();
                socketWatch.Close();
                CFlag = 0;
            }
            
            threadWatch.Abort();
            if (threadWatch.IsAlive) {
                btnListen.Enabled = true;
            }
            //CFlag = 0;
            //btnListen.Enabled = true;
            //rchReceive.Text += DateTime.Now.ToString("yy-MM-dd hh:mm:ss ");
            rchReceive.Text += GetTime();
            rchReceive.Text += "  服务器已关闭" + "\n";
            MessageBox.Show("服务器已关闭");
        }
        //按enter键发送消息
        private void rchSend_KeyDown(object sender, KeyEventArgs e) {
            if (e.KeyCode == Keys.Enter) {//如果输入的是回车键
                e.SuppressKeyPress = true;//执行发送而不换行
                this.btnSend_Click(sender, e);//触发button事件
            }
        }
        //取消发送
        private void button1_Click(object sender, EventArgs e) {
            rchSend.Clear();
        }
    }
}
/*服务器端：
 * ① 建立一个用于通信的Socket对象
 * ② 使用bind 绑定IP地址和端口号
 * ③ 使用listen 监听客户端
 * ④ 使用accept 中断程序直到连接上客户端
 * ⑤ 接收来自客户端的请求
 * ⑥ 返回客户端需要的数据
 * ⑦ 如果接收到客户端已关闭连接信息就关闭服务器
 */
/*客户端：
 * ① 建立一个用于通信的Socket对象
 * ② 根据指定的IP和端口 connect服务器
 * ③ 连接成功后向服务器端发送数据请求
 * ④ 接收服务器返回的请求数据
 * ⑤ 如果还需要请求数据继续发送请求
 * ⑥ 如果不需要请求数据就关闭客户端并给服务器发送关闭连接信息
 */
/* 通过while方式，我们可以实现多个客户端和服务端进行通讯。
 * 由于socket通信是阻塞式的，假设我现在有A和B两个客户端同时连接到服务端上，当客户端A发送信息给服务端后，
 * 那么服务端就一直阻塞在A的客户端上，不同的通过while循环从A客户端读取信息，此时如果B给服务端发送信息时，
 * 将进入阻塞队列，直到A客户端发送完毕并且退出后，B才可以和服务端进行通信。所以需要多线程的方式进行多客户端通讯。
 */